#include "box2d_test.h"
#include "../Box2D/Box2D.h"
#include "cstdlib"
#include "cstdio"
#include <jni.h>

//#define _MOUSE_JOINT_

int32 maxBodySize = 300;
b2Body* bodies[300]; //I have no idea
b2World* world = 0;

// Prepare for simulation. Typically we use a time step of 1/60 of a
// second (60Hz) and 10 iterations. This provides a high quality simulation
// in most game scenarios.
float32 timeStep = 1.0f / 60.0f;
int32 velocityIterations = 8; // 8
int32 positionIterations = 3; // 3

int32 bodyIndex = 0;

b2Body* mvBody = 0;
const b2Vec2 zero(0, 0);

b2Vec2 previousPos;
b2Vec2 currentPos;

void step(float32 ts, int32 vi, int32 pi) {

#ifndef _MOUSE_JOINT_
    if (mvBody != NULL) {
        b2Vec2 mv = currentPos - previousPos;
        mv *= 1.0f / ts;

        mvBody->SetLinearVelocity(mv);

        world->Step(ts, vi, pi);

        currentPos = previousPos = mvBody->GetPosition();
        mvBody->SetLinearVelocity(zero);
    }
#else
    if (mvBody != NULL) {
        mvBody->SetAngularVelocity(0);
        world->Step(ts, vi, pi);
    }
#endif
    else
        world->Step(ts, vi, pi);
}


class QueryCallback : public b2QueryCallback
{
public:
    b2Vec2 m_point;
    b2Fixture* m_fixture;

    QueryCallback(const b2Vec2& point)
    {
        m_point = point;
        m_fixture = NULL;
    }

    bool ReportFixture(b2Fixture* fixture)
    {
        b2Body* body = fixture->GetBody();
#ifndef _MOUSE_JOINT_
        if (body->GetType() == b2_dynamicBody)
#else
        if (body->GetType() == b2_kinematicBody)
#endif
        {
            bool inside = fixture->TestPoint(m_point);
            if (inside)
            {
                m_fixture = fixture;

                // We are done, terminate the query.
                return false;
            }
        }

        // Continue the query.
        return true;
    }
};

b2Body* createCircle(float x, float y, float radius, float weight,
        float restitution, float friction, b2BodyType type) {

    b2CircleShape circle;
    circle.m_radius = radius;

    b2FixtureDef fixDef;
    fixDef.shape = &circle;
    fixDef.density = weight;
    fixDef.friction = friction;
    fixDef.restitution = restitution;

    b2BodyDef ballDef;
    ballDef.position.Set(x, y);
    ballDef.type = type;
//    ballDef.awake = false;
    b2Body* body = world->CreateBody(&ballDef);

    // Create fixture
    body->CreateFixture(&fixDef);

    return body;
}

b2Body* createBox(float x, float y, float width, float height, float density,
        float restitution, float friction, b2BodyType type) {

    // Define the box shape.
    b2PolygonShape boxShape;
    boxShape.SetAsBox(width / 2.0f, height / 2.0f);

    b2FixtureDef fixDef;
    fixDef.shape = &boxShape;
    fixDef.density = density;
    fixDef.friction = friction;
    fixDef.restitution = restitution;

    b2BodyDef bodyDef;
    bodyDef.position.Set(x, y);
    bodyDef.type = type;

    // Call the body factory which allocates memory for the body
    // from a pool and creates the box shape (also from a pool).
    // The body is also added to the world.
    b2Body* body = world->CreateBody(&bodyDef);

    // Create fixture
    body->CreateFixture(&fixDef);

    return body;
}

extern "C" {

int32 findId(b2Body *body) {
    for (int32 i = 0; i < maxBodySize; i++) {
        if (bodies[i] == body) {
            return i;
        }
    }
    return -1;
}

void world_clear() {

    if (world != NULL) {
        for (int32 i = 0; i < maxBodySize; i++) {
            if (bodies[i] != NULL) {
                world->DestroyBody(bodies[i]);
                bodies[i] = NULL;
            }

        }
        delete world;
        world = NULL;
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_createWorld(
        JNIEnv* env, jobject caller, jfloat minX, jfloat minY, jfloat maxX,
        jfloat maxY, jfloat gravityX, jfloat gravityY) {

    //clear
    world_clear();
    bodyIndex = 0;

    // Define the gravity vector.
    b2Vec2 gravity(gravityX, gravityY);

    // Construct a world object, which will hold and simulate the rigid bodies.
    world = new b2World(gravity);
}

JNIEXPORT jint JNICALL Java_example_gl2jniforceball_JniInterface_createBox(
        JNIEnv* env, jobject caller, jfloat x, jfloat y, jfloat width,
        jfloat height) {

    bodies[bodyIndex] = createBox(x, y, width, height, 100.0f, 0.3f, 0, b2_staticBody);
    return bodyIndex++;
}

JNIEXPORT jint JNICALL Java_example_gl2jniforceball_JniInterface_createBoxPlayer(
        JNIEnv *env, jobject caller, jfloat x, jfloat y, jfloat width,
        jfloat height, jfloat density, jfloat restitution, jfloat friction) {

#ifndef _MOUSE_JOINT_
    bodies[bodyIndex] = createBox(x, y, width, height, density, restitution, friction, b2_dynamicBody);
#else
    bodies[bodyIndex] = createBox(x, y, width, height, density, restitution, friction, b2_kinematicBody);
#endif
    return bodyIndex++;

}

JNIEXPORT jint JNICALL Java_example_gl2jniforceball_JniInterface_createCirclePlayer(
        JNIEnv *env, jobject caller, jfloat x, jfloat y, jfloat radius, jfloat weight,
        jfloat restitution, jfloat friction) {

#ifndef _MOUSE_JOINT_
    bodies[bodyIndex] = createCircle(x, y, radius, weight, restitution, friction, b2_dynamicBody);
#else
    bodies[bodyIndex] = createCircle(x, y, radius, weight, restitution, friction, b2_kinematicBody);
#endif
    return bodyIndex++;

}

JNIEXPORT jint JNICALL Java_example_gl2jniforceball_JniInterface_createCircle(
        JNIEnv* env, jobject caller, jfloat x, jfloat y, jfloat radius,
        jfloat weight, jfloat restitution, jfloat friction) {

    bodies[bodyIndex] = createCircle(x, y, radius, weight, restitution, friction, b2_dynamicBody);
    return bodyIndex++;
}

JNIEXPORT jobject JNICALL Java_example_gl2jniforceball_JniInterface_getBodyInfo(
        JNIEnv* env, jobject caller, jobject bodyInfo, jint bindex) {

    if (bodies[bindex] == NULL) {
        return NULL;
    }
    jclass ballView = env->GetObjectClass(bodyInfo);
    jmethodID setValuesId = env->GetMethodID(ballView, "setValues", "(FFF)V");
    b2Body *body = bodies[bindex];
    env->CallVoidMethod(bodyInfo, setValuesId, body->GetPosition().x,
            body->GetPosition().y, body->GetAngle());

    return bodyInfo;
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_step(
        JNIEnv *env, jobject caller, jfloat timeStep, jint velocityIterations,
        jint positionIterations) {

    step(timeStep, velocityIterations, positionIterations);
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_setGravity(
        JNIEnv *env, jobject caller, jfloat gravityX, jfloat gravityY) {

    b2Vec2 gravity(gravityX, gravityY);
    world->SetGravity(gravity);
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_destroyBody(
        JNIEnv *env, jobject caller, jint id) {

    if (bodies[id] != NULL) {
        world->DestroyBody(bodies[id]);
        bodies[id] = NULL;
    }

}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_getCollisions(
        JNIEnv *env, jobject caller, jobject keeper, jint target) {

    jclass ballView = env->GetObjectClass(keeper);
    jmethodID addId = env->GetMethodID(ballView, "add", "(I)V");

    b2ContactEdge* c = bodies[target]->GetContactList();
    while (c != NULL) {
        int id = findId(c->other);
        if (id != -1) {
            env->CallVoidMethod(keeper, addId, id);
        }
        c = c->next;
    }

}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_setTransform(
        JNIEnv *env, jobject caller, jint id, jfloat x, jfloat y,
        jfloat angle) {

    if (bodies[id] != NULL) {
        b2Vec2 vec(x, y);
        bodies[id]->SetTransform(vec, angle);
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_setTranslate(
        JNIEnv *env, jobject caller, jint id, jfloat x, jfloat y) {

    if (bodies[id] != NULL) {
        b2Vec2 vec(x, y);
        bodies[id]->SetTransform(vec, 0);
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_setRotate(
        JNIEnv *env, jobject caller, jint id, jfloat angle) {

    if (bodies[id] != NULL) {
        bodies[id]->SetTransform(bodies[id]->GetPosition(), angle);
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_setBodyAngularVelocity(
        JNIEnv *env, jobject caller, jint id, jfloat angle) {

    if (bodies[id] != NULL) {
        bodies[id]->SetAngularVelocity(angle);
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_setBodyLinearVelocity(
        JNIEnv *env, jobject caller, jint id, jfloat x, jfloat y) {

    if (bodies[id] != NULL) {
        b2Vec2 vec(x, y);
        bodies[id]->SetLinearVelocity(vec);
    }
}

JNIEXPORT jobject JNICALL Java_example_gl2jniforceball_JniInterface_getStatus(
        JNIEnv *env, jobject caller, jobject bodyInfo, jint bindex) {

    if (bodies[bindex] == NULL) {
        return NULL;
    }
    jclass ballView = env->GetObjectClass(bodyInfo);
    jmethodID setValuesId = env->GetMethodID(ballView, "setStatus", "(ZZZ)V");
    b2Body *body = bodies[bindex];
    env->CallVoidMethod(bodyInfo, setValuesId, body->IsBullet(),
            !body->IsAwake(), !body->IsActive());

    return bodyInfo;

}

JNIEXPORT jobject JNICALL Java_example_gl2jniforceball_JniInterface_getLinerVelocity(
        JNIEnv *env, jobject caller, jobject bodyInfo, jint bindex) {

    if (bodies[bindex] == NULL) {
        return NULL;
    }
    jclass ballView = env->GetObjectClass(bodyInfo);
    jmethodID setValuesId = env->GetMethodID(ballView, "setLinerVelocity",
            "(FF)V");
    b2Body *body = bodies[bindex];
    env->CallVoidMethod(bodyInfo, setValuesId, body->GetLinearVelocity().x,
            body->GetLinearVelocity().y);

    return bodyInfo;
}

#ifndef _MOUSE_JOINT_

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_mouseDown(
        JNIEnv *env, jobject caller, jfloat x, jfloat y) {

    b2Vec2 p(x, y);

    // Make a small box.
    b2AABB aabb;
    b2Vec2 d;
    d.Set(5.0f, 5.0f);
    aabb.lowerBound = p - d;
    aabb.upperBound = p + d;

    // Query the world for overlapping shapes.
    QueryCallback callback(p);
    world->QueryAABB(&callback, aabb);

    if (callback.m_fixture)
    {
        mvBody = callback.m_fixture->GetBody();
        currentPos = previousPos = mvBody->GetPosition();
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_mouseUp(
        JNIEnv *env, jobject caller, jfloat x, jfloat y) {

    b2Vec2 p(x, y);

    if (mvBody != NULL)
    {
        b2Vec2 mv = p - previousPos;
        mv *= 10.0f;

        mvBody->SetLinearVelocity(mv);
        mvBody = NULL;
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_mouseMove(
        JNIEnv *env, jobject caller, jfloat x, jfloat y) {

    b2Vec2 p(x, y);

    if (mvBody != NULL)
    {
        currentPos = p;
    }
}
#else
b2MouseJoint* mouseJoint = NULL;

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_mouseDown(
        JNIEnv *env, jobject caller, jfloat x, jfloat y) {

    if (mouseJoint != NULL)
    {
        return;
    }

    b2Vec2 p(x, y);

    // Make a small box.
    b2AABB aabb;
    b2Vec2 d;
    d.Set(3.0f, 3.0f);
    aabb.lowerBound = p - d;
    aabb.upperBound = p + d;

    // Query the world for overlapping shapes.
    QueryCallback callback(p);
    world->QueryAABB(&callback, aabb);

    if (callback.m_fixture)
    {
        mvBody = callback.m_fixture->GetBody();
        mvBody->SetType(b2_dynamicBody);

        b2MouseJointDef md;
        md.bodyA = bodies[0];
        md.bodyB = mvBody;
        md.collideConnected = true;
        md.target = p;
        md.maxForce = 1000.0f * mvBody->GetMass();
        md.dampingRatio = 1.0f;
        md.frequencyHz = 1000;
        mouseJoint = (b2MouseJoint*)world->CreateJoint(&md);
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_mouseUp(
        JNIEnv *env, jobject caller, jfloat x, jfloat y) {

    if (mouseJoint != NULL)
    {
        mvBody->SetType(b2_kinematicBody);
        mvBody->SetLinearVelocity(zero);
        mvBody->SetAngularVelocity(0);
        mvBody = NULL;

        world->DestroyJoint(mouseJoint);
        mouseJoint = NULL;
    }
}

JNIEXPORT void JNICALL Java_example_gl2jniforceball_JniInterface_mouseMove(
        JNIEnv *env, jobject caller, jfloat x, jfloat y) {

    b2Vec2 p(x, y);

    if (mouseJoint != NULL)
    {
        mouseJoint->SetTarget(p);
    }
}
#endif

}
